#!/usr/bin/ruby

# APKBUILD dependency resolver for RubyGems
# Copyright (C) 2014-2016 Kaarle Ritvanen

require 'augeas'
require 'optparse'
require 'rubygems/dependency'
require 'rubygems/resolver'
require 'rubygems/spec_fetcher'

class Package
  @@packages = {}

  def self.initialize level
    @@augeas = Augeas::open(nil, nil, Augeas::NO_MODL_AUTOLOAD)
    dir = Dir.pwd
    @@augeas.transform(
      :lens => 'Shellvars.lns', :incl => dir + '/*/ruby*/APKBUILD'
    )
    @@augeas.load

    apath = '/files' + dir
    fail unless @@augeas.match("/augeas#{apath}//error").empty?

    repos = ['main', 'community', 'testing']
    repos = repos[0..repos.index(level)]
    for repo in repos
      for pkg in @@augeas.match "#{apath}/#{repo}/*"
        Aport.new(pkg) unless pkg.end_with? '/ruby'
      end
    end

    Subpackage.initialize @@augeas.get("#{apath}/main/ruby/APKBUILD/pkgver")

    @@packages.each_value do |pkg|
      pkg.depends do |dep|
        dep.add_user pkg
      end
    end
  end

  def self.get name
    pkg = @@packages[name]
    raise 'Invalid package name: ' + name unless pkg
    pkg
  end

  def self.save
    fail unless @@augeas.save
  end

  def initialize name
    @name = name
    @depends = []
    @users = []
    @@packages[name] = self
  end

  def add_dependency name
    @depends << name
  end

  attr_reader :name

  def depends
    for dep in @depends
      # ruby-gems: workaround for v2.6
      if dep.start_with?('ruby-') && dep != 'ruby-gems'
        unless @@packages.has_key? dep
          raise "Dependency for #{@name} does not exist: #{dep}"
        end
        yield @@packages[dep]
      end
    end
  end

  def users
    for user in @users
      yield user
    end
  end

  def add_user user
    @users << user
  end
end

class Aport < Package
  def initialize path
    super path.split('/')[-1]

    @path = path[6..-1]
    @apath = path + '/APKBUILD/'

    for dep in `echo #{get_param 'depends'}`.split
      add_dependency dep
    end
  end

  attr_reader :path

  def gem
    get_param '_gemname'
  end

  def version
    get_param 'pkgver'
  end

  def version= version
    set_param 'pkgver', version
    set_param 'pkgrel', '0'
  end

  def del_dependency name
    @depends.delete name
    set_param 'depends', "\"#{@depends.join ' '}\""
  end

  private

  def get_param name
    value = @@augeas.get(@apath + name)
    raise name + ' not defined for ' + @name unless value
    value
  end

  def set_param name, value
    @@augeas.set(@apath + name, value)
  end
end

class Subpackage < Package
  RUBY_SUBPACKAGES = {
    '2.0.0_p353' => {
      'ruby-minitest' => ['minitest', '4.3.2'],
      'ruby-rake' => ['rake', '0.9.6'],
      'ruby-rdoc' => ['rdoc', '4.0.0', 'ruby-json']
    },
    '2.0.0_p481' => {
      'ruby-minitest' => ['minitest', '4.3.2'],
      'ruby-rake' => ['rake', '0.9.6'],
      'ruby-rdoc' => ['rdoc', '4.0.0', 'ruby-json']
    },
    '2.1.5' => {
      'ruby-json' => ['json', '1.8.1'],
      'ruby-minitest' => ['minitest', '4.7.5'],
      'ruby-rake' => ['rake', '10.1.0'],
      'ruby-rdoc' => ['rdoc', '4.1.0', 'ruby-json']
    },
    '2.2.1' => {
      # it's actually 0.4.3 but that version is not published on network
      'ruby-io-console' => ['io-console', '0.4.2'],
      'ruby-json' => ['json', '1.8.1'],
      'ruby-minitest' => ['minitest', '5.4.3'],
      'ruby-rake' => ['rake', '10.4.2'],
      'ruby-rdoc' => ['rdoc', '4.2.0', 'ruby-json']
    },
    '2.2.2' => {
      # it's actually 0.4.3 but that version is not published on network
      'ruby-io-console' => ['io-console', '0.4.2'],
      'ruby-json' => ['json', '1.8.1'],
      'ruby-minitest' => ['minitest', '5.4.3'],
      'ruby-rake' => ['rake', '10.4.2'],
      'ruby-rdoc' => ['rdoc', '4.2.0', 'ruby-json']
    },
    '2.2.3' => {
      # it's actually 0.4.3 but that version is not published on network
      'ruby-io-console' => ['io-console', '0.4.2'],
      'ruby-json' => ['json', '1.8.1'],
      'ruby-minitest' => ['minitest', '5.4.3'],
      'ruby-rake' => ['rake', '10.4.2'],
      'ruby-rdoc' => ['rdoc', '4.2.0', 'ruby-json']
    },
    '2.2.4' => {
      # it's actually 0.4.3 but that version is not published on network
      'ruby-io-console' => ['io-console', '0.4.2'],
      'ruby-json' => ['json', '1.8.1'],
      'ruby-minitest' => ['minitest', '5.4.3'],
      'ruby-rake' => ['rake', '10.4.2'],
      'ruby-rdoc' => ['rdoc', '4.2.0', 'ruby-json']
    }
  }

  @@subpackages = []

  def self.initialize version
    for name, attrs in RUBY_SUBPACKAGES[version]
      new name, attrs
    end
  end

  def self.each
    for pkg in @@subpackages
      yield pkg
    end
  end

  def initialize name, attrs
    super name
    @gem, @version, *deps = attrs
    for dep in deps
      add_dependency dep
    end
    @@subpackages << self
  end

  attr_reader :gem, :version
end


class Update
  def initialize
    @gems = {}
    @deps = []
  end

  def require_version name, version
    gem = assign(Package.get(name).gem, name)
    @deps << gem.dependency if gem.require_version version
  end

  def resolve
    for pkg in Subpackage
      require_version pkg.name, pkg.version unless @gems[pkg.gem]
    end

    def check_deps
      @gems.clone.each_value do |gem|
        gem.check_deps
      end
    end

    check_deps

    for req in Gem::Resolver.new(@deps).resolve
      spec = req.spec
      gem = @gems[spec.name]
      gem.require_version spec.version.version if gem
    end

    check_deps

    for name, gem in @gems
      if gem.updated?
        gem.package.users do |user|
          ugem = @gems[user.gem]
          if !ugem || ugem.package.name != user.name
            Gem::Resolver.new(
              [gem.dependency, Gem::Dependency.new(user.gem, user.version)]
            ).resolve
          end
        end
      end
    end
  end

  def each
    @gems.each_value do |gem|
      update = gem.update
      yield update if update
    end
  end

  def assign name, package
    pkg = Package.get package

    if @gems.has_key? name
      gem = @gems[name]
      return gem if pkg == gem.package
      raise "Conflicting packages for gem #{name}: #{gem.package.name} and #{pkg.name}"
    end

    gem = PackagedGem.new self, name, pkg
    @gems[name] = gem
    gem
  end

  private

  class PackagedGem
    def initialize update, name, package
      @update = update
      @name = name
      @package = package
    end

    attr_reader :package

    def require_version version
      if @version
        return false if version == @version
        raise "Conflicting versions for gem #{@name}: #{@version} and #{version}"
      end
      @version = version
      true
    end

    def version
      @version || @package.version
    end

    def updated?
      version != @package.version
    end

    def dependency
      Gem::Dependency.new(@name, version)
    end

    def check_deps
      specs, errors = Gem::SpecFetcher::fetcher.spec_for_dependency(dependency)
      raise "Invalid gem: #{@name}-#{version}" if specs.empty?
      fail if specs.length > 1
      deps = specs[0][0].runtime_dependencies

      @obsolete_deps = []

      @package.depends do |dep|
        gem = @update.assign(dep.gem, dep.name)
        gem.check_deps
        unless deps.reject! { |sdep| sdep.match? dep.gem, gem.version }
          @obsolete_deps << dep.name
        end
      end

      unless deps.empty?
        raise 'Undeclared dependencies in ' + @package.name + deps.inject('') {
          |s, dep| "#{s}\n#{dep.name} #{dep.requirements_list.join ' '}"
        }
      end
    end

    def update
      updated? || !@obsolete_deps.empty? ? (
        {
          :name => @package.name,
          :version => version,
          :obsolete_deps => @obsolete_deps.clone,
          :path => @package.path
        }
      ) : nil
    end
  end
end


level = 'main'
update_files = nil
OptionParser.new do |opts|
  opts.on('-c', '--community') do |c|
    level = 'community'
  end
  opts.on('-t', '--testing') do |t|
    level = 'testing'
  end
  opts.on('-u', '--update') do |u|
    update_files = []
  end
end.parse! ARGV
Package.initialize level

latest = {}
for source, gems in Gem::SpecFetcher::fetcher.available_specs(:latest)[0]
  for gem in gems
    latest[gem.name] = gem.version.version
  end
end

update = Update.new
for arg in ARGV
  match = /^(([^-]|-[^\d])+)(-(\d.*))?/.match arg
  name = match[1]
  update.require_version name, match[4] || latest[Package.get(name).gem]
end

update.resolve

for pkg in update
  obsolete = pkg[:obsolete_deps]

  obs = obsolete.empty? ?
          nil : " (obsolete dependencies: #{obsolete.join ', '})"
  puts "#{pkg[:name]}-#{pkg[:version]}#{obs}"

  if update_files
    package = Package.get(pkg[:name])
    package.version = pkg[:version]
    for dep in obsolete
      package.del_dependency dep
    end
    update_files << pkg[:path]
  end
end

if update_files
  Package.save

  for path in update_files
    Dir.chdir(path) do
      fail unless system('abuild checksum')
    end
  end
end
